//-----------------------------------------------------------------------------
//
// CodeXS - Copyright(c) Businessware Architects cc
// Contact: Willem J Fourie
// E-mail : willemf@bware.biz
//
// This file is part of CodeXS.
//
// CodeXS is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License (GPL) as 
// published by the Free Software Foundation version 2 of the License.
//
// CodeXS is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with CodeXS; if not, write to:
//
// Free Software Foundation, Inc.
// 59 Temple Place
// Suite 330
// Boston
// MA  02111-1307  USA
//
//-----------------------------------------------------------------------------

using System;
using System.Text;
using System.Reflection;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.Xml.XPath;
using System.Collections;
using CodeXS.Schema;
using CodeXS.Schema.CodeBuilder;
using CodeXS.Schema.CodeModifier;
using CodeXS.Diagnostics;

namespace SchemaDocumentation
{
	public class Modifier : ICodeModifier
	{
		#region Implementation
		private Log log = null;
		public CodeXS.Diagnostics.Log Log
		{
			get { return log; }
			set { log = value; }
		}
		#region XmlSchemaDocumentItem
		public class XmlSchemaDocumentItem
		{
			private static string GetXmlText(string text)
			{
				text = text.Replace("&", "&amp;");
				text = text.Replace("\"", "&quot;");
				text = text.Replace("'", "&apos;");
				text = text.Replace("<", "&lt;");
				text = text.Replace(">", "&gt;");
				return text;
			}
			public static char ItemSeparator
			{
				get { return ';'; }
			}
			public static char AssignSymbol
			{
				get { return '='; }
			}
			public static char SectionSeparator
			{
				get { return '\t'; }
			}
			public enum XmlAttribute
			{
				Base,
				Name,
				Ref,
				Value,
				Type,
			}
			public enum XmlIgnoreType
			{
				All,
				Annotation,
				Choice,
				Documentation,
				Group,
				Restriction,
			}
			public enum XmlType
			{
				Attribute,
				AttributeGroup,
				ComplexType,
				Element,
				Enumeration,
				SimpleType,
				Undefined,
			}
			private string path = null;
			public string Path
			{
				get { return path; }
				set { path = value; }
			}
			private string documentation = null;
			public string Documentation
			{
				get { return documentation; }
				set 
				{
					documentation = GetXmlText(value); 
				}
			}
			public XmlSchemaDocumentItem() { }
			public XmlSchemaDocumentItem(string path, string documentation)
			{
				Path = path;
				Documentation = documentation;
			}
			public XmlSchemaDocumentItem(XmlSchemaDocumentItem item)
			{
				Path = item.Path;
				Documentation = item.Documentation;
			}
			public string GetAttribute(string nodeInfo, XmlAttribute attributeName,
				bool noNamespace)
			{
				string attributeValue = string.Empty;
				string[] attributes = nodeInfo.Split(new char[] {ItemSeparator});
				foreach (string attributeSection in attributes)
				{
					string[] attribute = attributeSection.Split(
						new char[] { AssignSymbol });
					if (attribute[0].Trim() == attributeName.ToString().ToLower())
					{
						attributeValue = attribute[1].Trim();
						break;
					}
				}
				if (noNamespace)
				{
					string[] nameSection = attributeValue.Split(new char[] { ';' });
					return nameSection[nameSection.Length - 1];
				}
				return attributeValue;
			}
			public string GetFirstAttributeSection(string nodeInfo)
			{
				string[] sections = nodeInfo.Split(new char[] { SectionSeparator });
				return sections[0];
			}
			public string GetNextAttributeInfo(string nodeInfo)
			{
				string remove = GetFirstAttributeSection(nodeInfo);
				nodeInfo = nodeInfo.Substring(remove.Length);
				nodeInfo = nodeInfo.TrimStart(new char[] { SectionSeparator });
				return nodeInfo;
			}
			public string GetLastAttributeSection(string nodeInfo)
			{
				string[] sections = nodeInfo.Split(new char[] { SectionSeparator });
				return sections[sections.Length - 1];
			}
			public string GetPriorAttributeInfo(string nodeInfo)
			{
				string remove = GetLastAttributeSection(nodeInfo);
				nodeInfo = nodeInfo.Substring(0, nodeInfo.Length - remove.Length);
				nodeInfo = nodeInfo.TrimEnd(new char[] { SectionSeparator });
				return nodeInfo;
			}
			public XmlType Type
			{
				get
				{
					string xmlType = GetAttribute(
						GetFirstAttributeSection(Path), XmlAttribute.Type, true);
					if (xmlType == string.Empty) return XmlType.Undefined;
					if (Enum.IsDefined(typeof(XmlType), GetEnumName(xmlType)))
					{
						return (XmlType)Enum.Parse(typeof(XmlType), xmlType, true);
					}
					return XmlType.Undefined;
				}
			}
			public string TypeName
			{
				get
				{
					return GetAttribute(GetFirstAttributeSection(Path),
						XmlAttribute.Name, true);
				}
			}
			public string GetEnumName(string valueName)
			{
				if ((valueName == null) ||
					(valueName == string.Empty))
				{
					return valueName;
				}
				return char.ToUpper(valueName[0]) + 
					valueName.Substring(1);
			}
			private bool IsXmlIgnoreType(string nodeInfoSection)
			{
				string xmlType = GetAttribute(nodeInfoSection,
					XmlAttribute.Type, true);
				if (xmlType == string.Empty) return false;
				return Enum.IsDefined(typeof(XmlIgnoreType), GetEnumName(xmlType));
			}
			public XmlType AttributeType
			{
				get
				{
					string info = Path;
					while(info.Length > 0)
					{
						string section = GetLastAttributeSection(info);
						if (info == section) break;
						if(IsXmlIgnoreType(section))
						{
							info = GetPriorAttributeInfo(info);
							continue;
						}
						string xmlType = GetAttribute(section, XmlAttribute.Type,
							true);
						if (xmlType == string.Empty) return XmlType.Undefined;
						if (Enum.IsDefined(typeof(XmlType), GetEnumName(xmlType)))
						{
							return (XmlType)Enum.Parse(typeof(XmlType), xmlType, true);
						}
					}
					return XmlType.Undefined;
				}
			}
			public string AttributeName
			{
				get
				{
					string info = Path;
					while (info.Length > 0)
					{
						string section = GetLastAttributeSection(info);
						if (info == section) break;
						if (IsXmlIgnoreType(section))
						{
							info = GetPriorAttributeInfo(info);
							continue;
						}
						// Check for element.
						string attributeName =
							GetAttribute(section, XmlAttribute.Name, true);
						if (attributeName != string.Empty) 
							return attributeName;
						// Check for ref
						attributeName =
							GetAttribute(section, XmlAttribute.Ref, true);
						if (attributeName != string.Empty)
							return attributeName;
						// Check for value
						return
							GetAttribute(section, XmlAttribute.Value, true);
					}
					return string.Empty;
				}
			}
			public static string CreateKey(string typeName, string attributeName)
			{
				return typeName + ":" + attributeName;
			}
			public string Key
			{
				get 
				{ 
					return CreateKey(TypeName, AttributeName);
				}
			}
			public bool IsEmptyKey
			{
				get
				{
					return (TypeName == string.Empty) &&
						(AttributeName == string.Empty);
				}
			}
		}
		#endregion XmlSchemaDocumentItem
		private string GetNodeInfo(XPathNavigator node)
		{
			StringBuilder path = new StringBuilder();
			path.Append(XmlSchemaDocumentItem.XmlAttribute.Type.ToString().ToLower());
			path.Append("=");
			path.Append(node.LocalName);
			string[] enums = Enum.GetNames(typeof(XmlSchemaDocumentItem.XmlAttribute));
			foreach(string attribute in enums)
			{
				if (attribute !=
					XmlSchemaDocumentItem.XmlAttribute.Type.ToString().ToLower())
				{
					string attributeValue = node.GetAttribute(
						attribute.ToLower(), string.Empty);
					if (attributeValue != string.Empty)
					{
						path.Append(XmlSchemaDocumentItem.ItemSeparator);
						path.Append(attribute.ToLower());
						path.Append("=");
						path.Append(attributeValue);
					}
				}
			}
			return path.ToString();
		}
		private Hashtable GetDocumentation(
			XPathNavigator navigator, Hashtable schemaDoc)
		{
			if (schemaDoc == null)
			{
				schemaDoc = new Hashtable();
			}
			XPathNodeIterator docNodes = navigator.SelectDescendants(
				"documentation", navigator.NamespaceURI, false);
			XmlSchemaDocumentItem lastItem = null;
			while (docNodes.MoveNext())
			{
				XPathNavigator docNode = 
					docNodes.Current.Clone();
				string documentation = docNode.Value;
				StringBuilder path = new StringBuilder(
					GetNodeInfo(docNode));
				while (docNode.MoveToParent())
				{
					if (docNode.LocalName == "schema") break;
					path.Insert(0, XmlSchemaDocumentItem.SectionSeparator);
					path.Insert(0, GetNodeInfo(docNode));
				}
				XmlSchemaDocumentItem item =
					new XmlSchemaDocumentItem(path.ToString(), documentation);
				if (item.IsEmptyKey) continue;
				if (lastItem != null)
				{
					if (item.Path == lastItem.Path)
					{
						lastItem.Documentation += "\r\n" + item.Documentation;
					}
					else
					{
						if (schemaDoc.ContainsKey(item.Key))
						{
							// Log.DefaultLog.LogMessageLine("{0} already added", item.Key);
						}
						else
						{
							schemaDoc.Add(item.Key, item);
						}
						lastItem = item;
					}
				}
				else
				{
					if (schemaDoc.ContainsKey(item.Key))
					{
						// Log.DefaultLog.LogMessageLine("{0} already added", item.Key);
					}
					else
					{
						schemaDoc.Add(item.Key, item);
					}
				}
			}
			return schemaDoc;
		}
		private XmlSchemaDocumentItem GetDocumentationItem(
			Hashtable documentation,
			string typeName,
			string attributeName)
		{
			if (attributeName == null) attributeName = string.Empty;
			string key = XmlSchemaDocumentItem.CreateKey(typeName,
				attributeName);
			XmlSchemaDocumentItem item = null;
			if (documentation.ContainsKey(key))
			{
				item = (XmlSchemaDocumentItem)documentation[key];
			}
			return item;
		}
		private XmlSchemaDocumentItem GetDocumentationItem(
			Hashtable documentation,
			CodeTypeDeclaration type,
			CodeTypeMember member)
		{
			if (member != null)
			{
				return GetDocumentationItem(documentation, 
					type.Name, member.Name);
			}
			else
			{
				return GetDocumentationItem(documentation,
					type.Name, null);
			}
		}
		private void UpdateDocumentation(CodeTypeDeclaration type,
			CodeTypeMember member, string xmlDocumentation)
		{
			// Special handling for enumeration fields.
			if (type.IsEnum && (member != null) && (member is CodeMemberField))
			{
				member.Comments.Clear();
			}
			CodeTypeMember construct = (member == null) ? type : member;
			if (xmlDocumentation.Length > 0)
			{
				// Change the existing documentation to remarks.
				string remarks = Documentation.GetDocumentationSection(construct,
					Documentation.Section.Remarks);
				if (remarks != string.Empty)
				{
					remarks = Documentation.CreateDocumentationBlock(
						Documentation.Section.Para, remarks);
				}
				string summary = Documentation.GetDocumentationSection(construct,
					Documentation.Section.Summary);
				if (summary != string.Empty)
				{
					if (remarks != string.Empty)
					{
						remarks = Documentation.CreateDocumentationBlock(
							Documentation.Section.Para, summary) + "\r" + remarks;
						Documentation.ReplaceDocumentationSection(construct,
							Documentation.Section.Remarks, remarks);
					}
					else
					{
						remarks = summary;
						Documentation.AddDocumentationSection(construct,
							Documentation.CreateDocumentationBlock(
								Documentation.Section.Remarks, remarks));
					}
				}
				Documentation.ReplaceDocumentationSection(construct,
					Documentation.Section.Summary, xmlDocumentation);
			}
		}
		private Hashtable GetDocumentation(string xsdUri, Hashtable documentation)
		{
			if (!documentation.ContainsKey(xsdUri))
			{
				Log.DefaultLog.LogMessageLine("--- Including schema: {0}", xsdUri);
				XPathDocument xDoc = new XPathDocument(xsdUri);
				XPathNavigator xNav = xDoc.CreateNavigator();
				XPathNodeIterator schemaNode = xNav.SelectDescendants(
					"schema", "http://www.w3.org/2001/XMLSchema", false);
				schemaNode.MoveNext();
				xNav = schemaNode.Current;
				documentation = GetDocumentation(xNav, documentation);
				documentation.Add(xsdUri, "schema-path");
			}
			return documentation;
		}
		private Hashtable GetDocumentation(CodeXS.Schema.CodeGenerator CodeGenerator,
			string xsdUri)
		{
			Log.DefaultLog.LogMessageLine(
				"Extracting documentation for schema: {0}", xsdUri);
			Hashtable documentation = GetDocumentation(xsdUri, new Hashtable());
			XmlSchema schema = CodeGenerator.GetSchema(xsdUri);
			foreach (XmlSchemaImport importSchema in schema.Includes)
			{
				// Cannot use XmlSchemaImport.Parent in .NET 1.1.
				Uri parentUri = new Uri(CodeGenerator.CurrentUriPath);
				Uri uri = new Uri(parentUri, importSchema.SchemaLocation);
				if (uri.IsFile) xsdUri = uri.LocalPath;
				else xsdUri = uri.AbsoluteUri;
				documentation = GetDocumentation(xsdUri, documentation);
			}
			return documentation;
		}
		private void UpdateDocumentation(CodeXS.Schema.CodeGenerator CodeGenerator,
			string xsdUri)
		{
			Hashtable documentation = GetDocumentation(CodeGenerator, xsdUri);
			foreach (CodeTypeDeclaration type in CodeGenerator.CodeNamespace.Types)
			{
				XmlSchemaDocumentItem item =
					GetDocumentationItem(documentation, type, null);
				if (item != null)
				{
					UpdateDocumentation(type, null, item.Documentation);
				}
				foreach (CodeTypeMember member in type.Members)
				{
					item = GetDocumentationItem(documentation, type, member);
					if (item != null)
					{
						UpdateDocumentation(type, member, item.Documentation);
					}
				}
			}
		}
		#endregion Implementation
		#region ICodeModifier Members
		public string Name
		{
			get { return Defaults.ModifierName(this); }
		}
		public string Version
		{
			get { return Information.Version(); }
		}
		public void Execute(CodeXS.Schema.CodeGenerator CodeGenerator)
		{
			string xsdUri = CodeGenerator.CurrentUriPath;
			try
			{
				UpdateDocumentation(CodeGenerator, xsdUri);
			}
			catch (Exception ex)
			{
				Log.DefaultLog.LogException(ex);
			}
		}
		public bool Enabled
		{
			get { return true; }
		}
		public bool RunOnce
		{
			get { return false; }
		}
		public void Initialize(CodeXS.Schema.CodeGenerator CodeGenerator)
		{
			this.Log = CodeGenerator.Log;
		}
		public void Deinitialize(CodeXS.Schema.CodeGenerator CodeGenerator) 
		{
			this.Log = null;
		}
		public object SchemaImporterExtension
		{ 
			get { return null; }
		}
		#endregion ICodeModifier Members
	}
}